﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.IO;
using System.Reflection.Emit;
using System.Diagnostics.SymbolStore;
using Kokomo.PeInspector.CliMetadata;
using Kokomo.PeInspector.Reflection.Disassembly;
using Kokomo.PeInspector.VisualStudioAdapter;
using System.Text.RegularExpressions;
using Kokomo.Mirror.CliMetadata.DebugInfo;
using Kokomo.Mirror.CliMetadata;

namespace Kokomo.PeInspector.Reflection
{
	class AssemblyExporter
	{
		public string OutputPath { get; private set; }
		public Assembly Assembly { get; private set; }
		public Type[] Types { get; private set; }

		public string[] AssemblyInfoImports { get; set; }
		public string[] SourceDocumentImports { get; set; }

		public bool IncludeResources { get; set; }

		public AssemblyExporter(
			Assembly assembly,
			Type[] types,
			IAssemblyLoadContext loadContext,
			string outputPath
			)
		{
			if (assembly == null) throw new ArgumentNullException("assembly");
			if (outputPath == null) throw new ArgumentNullException("outputPath");
			if (types == null) throw new ArgumentNullException("types");
			if (loadContext == null) throw new ArgumentNullException("loadContext");

			this.LoadContext = loadContext;

			if (!Directory.Exists(outputPath)) throw new ArgumentException("Output directory must already exist", "outputPath");

			this.Assembly = assembly;
			this.OutputPath = outputPath;
			this.Types = types;

			AssemblyName name = assembly.GetName();

			this._debugDatabaseBuilder = new DebugDatabaseBuilder(assembly.EntryPoint);
		}

		private DebugDatabaseBuilder _debugDatabaseBuilder;

		public IAssemblyLoadContext LoadContext { get; private set; }

		private string GetProjectRelativePath(string ns, string className)
		{
			string directory = string.Empty;
			if (ns != null)
				directory = Path.Combine(directory, ns.Replace('.', '\\'));

			StringBuilder fileNameBuilder = new StringBuilder();
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			foreach (var c in className)
			{
				bool isInvalid = invalidFileNameChars.Contains(c);
				fileNameBuilder.Append(isInvalid ? '_' : c);
			}
			fileNameBuilder.Append(".cs");
			string fileName = fileNameBuilder.ToString();

			string path = Path.Combine(directory, fileName);

			return path;
		}

		public AssemblyExporterResult Export(CSharpProject project, ProgressInfo progress = null)
		{
			if (project == null) throw new ArgumentNullException("project");

			AssemblyName assemblyName = this.Assembly.GetName();

			IList<Type> types = this.Types;
			string[] resourceNames = this.Assembly.GetManifestResourceNames();
			if (progress != null) progress.Maximum = types.Count + resourceNames.Length;
			string projectFileName = Path.Combine(this.OutputPath, assemblyName.Name + ".csproj");
			AssemblyExporterResult result = new AssemblyExporterResult()
			{
				ProjectFilePath = projectFileName,
				OutputDirectory = this.OutputPath
			};

			// Create project
			if (progress != null) progress.Text = UiString.FromString(Messages.CreatingProjectText);
			BasicPropertyGroup basicProperties = project.ProjectProperties;
			basicProperties.ProjectGuid = Guid.NewGuid();
			basicProperties.OutputType = "Library";
			basicProperties.AppDesignerFolder = "Properties";
			basicProperties.RootNamespace = assemblyName.Name;
			basicProperties.AssemblyName = assemblyName.Name;

			Match versionMatch = Regex.Match(this.Assembly.ImageRuntimeVersion, @"(?<=v)\d+\.\d+");
			string targetFrameworkVersionCode = (versionMatch.Success) ? "v" + versionMatch.Value : "4.0";
			basicProperties.TargetFrameworkVersion = targetFrameworkVersionCode;

			// Add project references
			AssemblyName[] referenceNames = this.Assembly.GetReferencedAssemblies();
			foreach (var referenceName in referenceNames)
			{
				ReferenceItem reference = new ReferenceItem() { Include = referenceName.FullName };
				string hintPath = null;
				try
				{
					Assembly referenced = this.LoadContext.LoadAssembly(referenceName, this.Assembly);
					bool isGac = referenced.GlobalAssemblyCache;
					if (!isGac)
					{
						hintPath = referenced.Location;
					}
				}
				catch { }

				reference.HintPath = hintPath;
				project.ReferenceItems.Add(reference);
			}

			// Write attributes
			string assemblyInfoPath = Path.Combine(this.OutputPath, this.GetProjectRelativePath("Properties", "AssemblyInfo"));
			string appDesignerDirectory = Path.GetDirectoryName(assemblyInfoPath);
			if (!Directory.Exists(appDesignerDirectory)) Directory.CreateDirectory(appDesignerDirectory);
			result.AssemblyInfoPath = assemblyInfoPath;
			using (StreamWriter streamWriter = new StreamWriter(assemblyInfoPath))
			{
				TextCodeWriter codeWriter = new TextCodeWriter(streamWriter);

				CSharpDocumentBuilder codeBuilder = new CSharpDocumentBuilder(codeWriter, null, null, this.AssemblyInfoImports);

				codeBuilder.WriteDeclarationAttributes(this.Assembly);

				project.CompileItems.Add(new CompileItem() { Include = assemblyInfoPath });
			}

			// Write type stuff
			for (int i = 0; i < types.Count; i++)
			{
				Type type = types[i];
				if (progress != null)
				{
					progress.Progress++;
					progress.Text = UiString.FromString(Messages.ExportingTypeLabel + ": " + type.QualifiedFullName());
				}

				if (type.IsAnonymous() || type.IsNested) continue;

				string sourceDocumentRelativePath = this.GetProjectRelativePath(type.Namespace, type.Name);
				string sourceDocumentPath = Path.Combine(this.OutputPath, sourceDocumentRelativePath);
				string sourceDocumentDirectory = Path.GetDirectoryName(sourceDocumentPath);

				if (!Directory.Exists(sourceDocumentDirectory)) Directory.CreateDirectory(sourceDocumentDirectory);

				project.CompileItems.Add(new CompileItem() { Include = sourceDocumentRelativePath });

				if (progress != null) progress.Text = UiString.FromString(Messages.ExportingTypeLabel + ": " + type.QualifiedFullName());

				using (StreamWriter streamWriter = new StreamWriter(sourceDocumentPath))
				{
					SymDocumentBuilder symbolDocumentBuilder = this._debugDatabaseBuilder.DefineDocument();
					symbolDocumentBuilder.Url = sourceDocumentPath;
					//documentBuilder.Language=?;
					//documentBuilder.LanguageVendor=?;
					//documentBuilder.DocumentType=?;

					//symbolWriter.Initialize(IntPtr.Zero, @"C:\output\symbols.pbd", true);
					TextCodeWriter codeWriter = new TextCodeWriter(streamWriter);

					CSharpDocumentBuilder codeBuilder = new CSharpDocumentBuilder(codeWriter, type, MsilDecompiler.Decompile, this.SourceDocumentImports);
					//SourceInfoCollector collector = new SourceInfoCollector(codeWriter, codeBuilder, symbolDocumentBuilder);

					codeBuilder.MethodBodyStart += new EventHandler<MethodBodyEventArgs>(codeBuilder_MethodBodyStart);

					codeBuilder.WriteTypeDeclaration(type);
				}

				TypeExportResult detail = new TypeExportResult(sourceDocumentRelativePath, type);
				result.Details.Add(detail);
			}

			// Write resources
			if (this.IncludeResources)
			{
				foreach (var resourceName in resourceNames)
				{
					if (progress != null)
					{
						progress.Progress++;
						progress.Text = UiString.FromString(Messages.ExportingResourceLabel + ": " + resourceName);
					}

					Stream resourceStream = this.Assembly.GetManifestResourceStream(resourceName);
					byte[] resourceData = new byte[resourceStream.Length];
					resourceStream.Read(resourceData, 0, resourceData.Length);

					string resourceFileName = Path.Combine(this.OutputPath, resourceName);
					using (FileStream resourceFile = new FileStream(resourceFileName, FileMode.Create))
					{
						resourceFile.Write(resourceData, 0, resourceData.Length);
					}

					project.ResourceItems.Add(new ResourceItem() { Include = resourceName });

					ResourceExportResult detail = new ResourceExportResult(resourceName, resourceName);
					result.Details.Add(detail);
				}
			}

			this._debugDatabaseBuilder.Save(Path.Combine(this.OutputPath, assemblyName.Name + ".pdb"));

			project.Save(projectFileName);

			return result;
		}

		void codeBuilder_MethodBodyStart(object sender, MethodBodyEventArgs e)
		{
		}
	}
}
